A pane with each worktree team's build lead run, its step and subagents, this session's subagents, and the 5-hour, 7-day and context usage

A web and command-line discounted cash flow (DCF) valuation tool. An LLM extracts the financial statements and the non-recurring items from SEC 10-K filings; deterministic Python checks the reading and does every calculation after it. (A 10-Q stops at the fiscal-year check today: backlog item 57.)
The LLM is strictly an extraction layer — it reads numbers from PDFs. All projections, adjustments, and valuation math are handled by auditable, deterministic code.
This project runs on Windows and macOS. The interpreter lives under .venv/Scripts/ on Windows and under .venv/bin/ on macOS. Every command below uses the Windows path. On macOS, replace .venv/Scripts/python.exe with .venv/bin/python. Never a bare python. docs/8-build/environment.md owns the setup and the gates.
git clone <repo-url>
cd valuation_platform
.venv/Scripts/python.exe -m pip install -r requirements.txt
Create a .env file in the project root:
GEMINI_API_KEY=your-gemini-api-key-here
Route A reads GEMINI_API_KEY only. Route B needs no key in the app: a Claude Code session writes the session file (.claude/skills/extract-filing/SKILL.md).
.venv/Scripts/python.exe -m uvicorn app:app --reload
Then open http://localhost:8000 in your browser.
The repository holds one route B session file, three Walmart 10-Ks read by a Claude Code session. It runs the whole valuation with no model call (yfinance still fetches the market data):
.venv/Scripts/python.exe cli.py --session-file extractions/WMT.json
A PDF upload in the web app uses route A and makes paid Gemini calls.
Put the company's 10-K PDFs in a folder named for its ticker:
10K_filings/
└── LLY/
├── LLY_10K_2023-12-31.pdf
├── LLY_10K_2024-12-31.pdf
└── LLY_10K_2025-12-31_English.pdf
Then pass only the folder. The ticker is inferred from the folder name and the fiscal year from the first four digits of each filing date:
.venv/Scripts/python.exe cli.py 10K_filings/LLY
Both 10K and 10-K are accepted in filenames. The existing explicit form also remains available:
.venv/Scripts/python.exe cli.py 2023:10K_2023.pdf 2024:10K_2024.pdf -t LLY
valuation_platform/
├── app.py # FastAPI entry point
├── cli.py # Command-line entry point
├── pipeline.py # The one valuation sequence both entry points call
├── config.py # Default assumptions and project paths
├── gates.py # Runs every gate (22) against .agent/gates.json
├── ingestion/
│ ├── claude_extractor.py # Route A extraction (two passes) and the checks of a reading
│ ├── session_extraction.py # Route B: loads and checks a Claude Code session file
│ ├── filings.py # Filing discovery and hashing
│ └── price_fetcher.py # Stock/market price data via yfinance
├── analysis/
│ ├── normalizer.py # GAAP → Non-GAAP adjustments using extracted non-recurring items
│ ├── capm.py # CAPM beta regression (stock vs S&P 500)
│ ├── wacc.py # Weighted average cost of capital
│ ├── projector.py # Derive assumptions from historicals & project future financials
│ ├── fcff.py # Free cash flow to firm calculation
│ └── dcf.py # Discounted cash flow valuation with terminal value
├── models/
│ ├── financial_statements.py # Core dataclasses (FinancialStatements, NonRecurringItem)
│ ├── valuation.py # ProjectionAssumptions, DCFResult
│ ├── market_data.py # PriceData
│ └── stops.py # Stop: the exception of every planned stop
├── api/
│ ├── routes_upload.py # PDF upload endpoint
│ └── routes_valuation.py # Assumptions form & valuation execution
├── templates/ # Jinja2 HTML templates
├── static/ # CSS
├── extractions/ # Route B session files (WMT.json)
└── tests/unit/ # Unit tests (4173 at a8bacaf), run by gates.py
10-K PDF (route A) or session file (route B)
→ LLM Pass 1: Extract the printed rows of the statements (I/S, C/F, B/S)
→ LLM Pass 2: Identify non-recurring items from MD&A and footnotes
→ Python checks the reading (sums, pages, units, rows); route A retries a failed check up to 2 times
→ Normalize financials (apply non-recurring adjustments)
→ Fetch stock & market returns (yfinance)
→ CAPM beta regression → Cost of equity
→ WACC calculation
→ Project future FCFFs
→ DCF with terminal value → Implied share price
CFO + Interest*(1-t) - CapEx); projected uses EBIT-based (EBIT*(1-t) + D&A - CapEx - dNWC)hooks/register.tsx 243 lines1import { atom, read, update } from 'claude-code'
2import type {
3 CommandPresentation,
4 EngineInterface,
5 PaneOpenArgs,
6 Register,
7 SessionContextUsage,
8 SessionRateLimit,
9} from 'claude-code'
10
11import type { AgentView, Board, JournalRow, TeamRow, UsageView } from '../types'
12
13// The pane: one per session, drawn by the ui.render hook below. Its place is the dock
14// beside the transcript, which only the fullscreen layout has, from 110 columns.
15const PANE = 'team-board'
16const OPEN: PaneOpenArgs = { id: PANE, title: 'Teams and usage', columns: 56 }
17const DOCK_FLOOR = 110
18const REFRESH_MS = 10_000
19
20// The board comes from the repository's own script, run in the main checkout's root.
21// The session's working directory can be a subfolder or a worktree that has no .venv.
22function script(root: string): string[] {
23 return [`${root}/.venv/bin/python`, `${root}/.claude/build-lead/team_status.py`, 'show', '--json']
24}
25
26const board = atom({ plugin: 'team-board', key: 'board' } as const, { generatedAt: 0, teams: [] } as Board)
27const usage = atom({ plugin: 'team-board', key: 'usage' } as const, { limits: [] } as UsageView)
28const agents = atom({ plugin: 'team-board', key: 'agents' } as const, [] as AgentView[])
29
30const LIVE = new Set(['running', 'pending', 'waiting'])
31const FINAL = /^(status: (complete|pass)|verdict: (approved|pass))\b/
32
33function toUsage(context: SessionContextUsage, limits: SessionRateLimit[]): UsageView {
34 return {
35 contextPercent: context.percent,
36 contextTokens: context.tokens,
37 contextWindow: context.window,
38 limits: limits.map(l => ({ kind: l.kind, percentUsed: l.percentUsed, resetsAt: l.resetsAt })),
39 }
40}
41
42function clockTime(ms: number): string {
43 const d = new Date(ms)
44 return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
45}
46
47function untilText(ms: number, now: number): string {
48 const minutes = Math.max(0, Math.round((ms - now) / 60_000))
49 return minutes >= 60 ? `${Math.floor(minutes / 60)}h ${minutes % 60}m` : `${minutes}m`
50}
51
52function journalMark(row: JournalRow): string {
53 if (FINAL.test(row.status)) return `${row.role} done`
54 if (row.status === '') return `${row.role} ?`
55 return `${row.role} ${row.status.replace(/^(status|verdict): /, '')}`
56}
57
58function placementText(p: CommandPresentation): string {
59 if (!p.isFullscreen) {
60 return 'Team board opened above the prompt. The dock beside the transcript needs the fullscreen layout: run /tui fullscreen, which restarts and resumes this session.'
61 }
62 if (p.columns < DOCK_FLOOR) {
63 return `Team board opened above the prompt. The dock beside the transcript needs ${DOCK_FLOOR} columns; the terminal has ${p.columns}.`
64 }
65
66 return 'Team board opened in the dock beside the transcript and refreshed.'
67}
68
69function stateColor(team: TeamRow): 'success' | 'warning' | 'error' | 'subtle' {
70 if (team.state === 'running') return 'warning'
71 if (team.state === 'handed off') return 'success'
72 if (team.state === 'blocked' || team.state.startsWith('stopped')) return 'error'
73 return 'subtle'
74}
75
76let isBusy = false
77let isOpenedUnasked = false
78
79async function refresh($: EngineInterface): Promise<void> {
80 if (isBusy) return
81 isBusy = true
82 try {
83 try {
84 const repo = await $.session.repo()
85 if (!repo) throw new Error('the session is not in a git repository')
86 const ran = await $.process.run(script(repo.root), {
87 cwd: repo.root,
88 env: { ANTHROPIC_API_KEY: '', GEMINI_API_KEY: '' },
89 timeoutMs: 25_000,
90 })
91 const parsed = ran.exitCode === 0 ? JSON.parse(ran.stdout) : undefined
92 const now = await $.clock.now()
93 await update($, board, previous =>
94 parsed
95 ? { generatedAt: now, teams: parsed.teams as TeamRow[] }
96 : { ...previous, error: `team_status.py exited ${ran.exitCode}: ${ran.stderr.trim().split('\n').pop() ?? ''}` },
97 )
98 } catch (error) {
99 await update($, board, previous => ({
100 ...previous,
101 error: `cannot read the board here: ${String(error).slice(0, 120)}`,
102 }))
103 }
104 const measured = await $.session.usage()
105 await update($, usage, () => toUsage(measured.context, measured.rateLimits))
106 const list = await $.agent.list()
107 await update($, agents, () =>
108 list.map(a => ({ id: a.id, type: a.type, description: a.description, status: a.status })),
109 )
110 } finally {
111 isBusy = false
112 }
113}
114
115export const register: Register = on => {
116 on('session.start', async ($, e, next) => {
117 await $.command.register({
118 name: 'team-board',
119 description: 'Show each worktree team, its step and subagents, and the 5-hour, 7-day and context usage',
120 })
121 void refresh($)
122 $.clock.every(REFRESH_MS, () => void refresh($))
123
124 return next(e)
125 })
126
127 on('command.run', { command: 'team-board' }, async ($, e) => {
128 await $.ui.open(OPEN)
129 await refresh($)
130
131 return { text: placementText(e.presentation) }
132 })
133
134 // Only a render says whether the surface docks a pane. Unasked, the pane opens only
135 // where it docks: on the main screen it would take the rows above the prompt.
136 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
137 if (!isOpenedUnasked && e.viewport?.isFullscreen === true) {
138 isOpenedUnasked = true
139 void $.ui.open(OPEN)
140 }
141
142 return next(e)
143 })
144
145 on('session.measure', async ($, e, next) => {
146 await update($, usage, () => toUsage(e.context, e.rateLimits))
147
148 return next(e)
149 })
150
151 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
152 const { Box, Text } = $.ui.resolve(e)
153 const b = await read($, board)
154 const u = await read($, usage)
155 const live = (await read($, agents)).filter(a => LIVE.has(a.status))
156 const now = b.generatedAt || (await $.clock.now())
157 const five = u.limits.find(l => l.kind === 'five_hour')
158 const seven = u.limits.find(l => l.kind === 'seven_day')
159
160 return (
161 <Box flexDirection="column">
162 <Text bold>Teams{b.generatedAt ? <Text dimColor> updated {clockTime(b.generatedAt)}</Text> : ''}</Text>
163 {b.error && <Text color="error" wrap="truncate-end">{b.error}</Text>}
164 {b.teams.length === 0 && !b.error && <Text dimColor>No registered team run.</Text>}
165 {b.teams.map(team => (
166 <Box key={`team-${team.team}`} flexDirection="column" marginTop={1}>
167 <Text wrap="truncate-end">
168 <Text bold>Team {team.team}</Text> {team.unit}
169 </Text>
170 <Text wrap="truncate-end">
171 {' '}
172 <Text color={stateColor(team)}>{team.state}</Text>
173 <Text dimColor>
174 {' '}· {team.route} {team.model ?? ''} {team.effort ?? ''}
175 {team.elapsed ? ` · ${team.elapsed}` : ''}
176 </Text>
177 </Text>
178 {team.state === 'running' && (
179 <Text wrap="truncate-end">
180 {' '}step: <Text bold>{team.step}</Text>
181 </Text>
182 )}
183 {team.running_subagents.map((s, i) => (
184 <Text key={`sub-${team.team}-${i}`} wrap="truncate-end">
185 {' '}
186 <Text color="warning">runs</Text> {s.role}
187 <Text dimColor> {s.description}</Text>
188 </Text>
189 ))}
190 {team.journal.length > 0 && (
191 <Text dimColor wrap="truncate-end">
192 {' '}
193 {team.journal.map(journalMark).join(' · ')}
194 </Text>
195 )}
196 <Text dimColor wrap="truncate-end">
197 {' '}commits {team.commits} · uncommitted {team.uncommitted}
198 {team.agy_quota_resets_at
199 ? ` · agy quota resets ${clockTime(team.agy_quota_resets_at * 1000)}`
200 : ''}
201 </Text>
202 </Box>
203 ))}
204 <Text bold>{'\n'}This session's subagents</Text>
205 {live.length === 0 && <Text dimColor>none running</Text>}
206 {live.map(a => (
207 <Text key={`agent-${a.id}`} wrap="truncate-end">
208 <Text color="warning">{a.status}</Text> {a.type}
209 <Text dimColor> {a.description}</Text>
210 </Text>
211 ))}
212 <Text bold>{'\n'}Usage</Text>
213 <Text wrap="truncate-end">
214 5h{' '}
215 {five ? `${five.percentUsed}% used` : <Text dimColor>no reading yet</Text>}
216 {five?.resetsAt ? (
217 <Text dimColor>
218 {' '}· resets {clockTime(Date.parse(five.resetsAt))} (in {untilText(Date.parse(five.resetsAt), now)})
219 </Text>
220 ) : (
221 ''
222 )}
223 </Text>
224 <Text wrap="truncate-end">
225 7d{' '}
226 {seven ? `${seven.percentUsed}% used` : <Text dimColor>no reading yet</Text>}
227 {seven?.resetsAt ? <Text dimColor> · resets {new Date(seven.resetsAt).toDateString()}</Text> : ''}
228 </Text>
229 <Text wrap="truncate-end">
230 ctx {u.contextPercent !== undefined ? `${u.contextPercent}%` : <Text dimColor>no reading yet</Text>}
231 {u.contextTokens !== undefined && u.contextWindow ? (
232 <Text dimColor>
233 {' '}· {Math.round(u.contextTokens / 1000)}k of {Math.round(u.contextWindow / 1000)}k
234 </Text>
235 ) : (
236 ''
237 )}
238 </Text>
239 </Box>
240 )
241 })
242}
243types/index.d.ts 29 lines1export type JournalRow = { file: string; role: string; status: string }
2export type SubagentRow = { role: string; description: string }
3export type TeamRow = {
4 team: string
5 unit: string
6 route: string
7 model: string | null
8 effort: string | null
9 state: string
10 step: string
11 elapsed: string | null
12 running_subagents: SubagentRow[]
13 subagents_done: number
14 journal: JournalRow[]
15 commits: number
16 uncommitted: number
17 agy_quota_resets_at: number | null
18}
19export type Board = { generatedAt: number; teams: TeamRow[]; error?: string }
20export type LimitView = { kind: string; percentUsed: number; resetsAt?: string }
21export type UsageView = { contextPercent?: number; contextTokens?: number; contextWindow?: number; limits: LimitView[] }
22export type AgentView = { id: string; type: string; description: string; status: string }
23
24declare module 'claude-code' {
25 interface PluginState {
26 'team-board': { board: Board; usage: UsageView; agents: AgentView[] }
27 }
28}
29