/indicator — parallel TDD swarm for a new market indicator
Arguments: <data-source> (URL or short description) and <indicator-name>.
Prerequisite: read .claude/skills/new-indicator/SKILL.md first — it defines the
target pattern, the lockstep pins, and the CI gates. Every step below produces
verifiable evidence before the next step starts.
Derive up front and state them: slug (route segment, short), service (kebab-case,
used for scan_snapshots.service, service_health, and systemd unit names), Name
(PascalCase for components), tab label (UPPERCASE, no em dashes), and the next free
migration number (ls scripts/db/migrations | sort | tail -1).
Step 1 — Research subagent (blocking)
Spawn one research subagent (general-purpose; give it WebFetch/curl) to confirm, with
evidence pasted back:
- Source: exact URL(s), transport (XML/JSON/CSV/XLSX), auth (must be none or an
existing repo credential), and a captured sample saved to
scripts/tests/fixtures/<name>_sample.<ext> (this becomes the pytest fixture).
- Schema: field names, units, date keying, history depth, how far back a backfill
can go, and any splice/seam issues between historical regimes.
- Update cadence: when the source actually publishes (day of week/month, time,
timezone) — this dictates the timer OnCalendar,
MAX_AGE_MS, staleness windows,
and what freshness copy is honest.
- Licensing: US-government/public-domain data is fine; otherwise confirm terms
permit storage + display. Record verdict in the spec. Stop and ask the user if
licensing is unclear.
- Data-source priority: note why IB/UW do not already serve this series (repo rule:
IB → UW → Yahoo → scrape).
Reject the indicator here if the source needs browser impersonation, scraping behind
auth, or has hostile terms.
Step 2 — Spec + failing tests (red)
Write docs/indicators/<slug>.md: signal definition and thresholds, source facts from
Step 1, payload shape (scan_time, source_last_modified, current, series[]),
migration DDL, API contract (missing object, cache headers, MAX_AGE reasoning), UI spec
(strip cells, chart series/axes, presets, copy strings including tooltip), the
freshness rail (the <NAME>_REFRESH constant that mirrors the timer's
OnCalendar, the <FreshnessRail> mount under the strip with its two testIds, and
which payload field feeds asOf), timer cadence, and the exact file checklist per
the pattern skill. The countdown is not optional: an indicator with a timer always
shows when its source is next sampled, and the spec must name the constant.
Then write the failing tests against the spec (implementation files do not exist yet):
scripts/tests/test_<name>.py — parse the captured fixture, transforms, payload
contract, migration schema pin, conditional-GET/heartbeat behavior, writer arity.
web/tests/<name>-api.test.ts — dbFirstRead behavior + missing contract.
web/tests/<name>-panel.test.tsx — loader/empty/strip/chart/chips + NaN guard.
Run all three suites and record the red output. Failing on import/missing-module is
the expected red for greenfield files.
Step 3 — Three worktrees, parallel implementers
Partition by ownership so merges are near-disjoint:
| Worktree branch |
Owns |
Its tests |
ind/<slug>-ingestion |
scripts/fetch_<name>.py, client, migration, scripts/db/writer.py additions, scripts/watchdog/services.py, cloud/services/radon-<name>.{service,timer}, setup-vps.sh array, cloud/tests/test_systemd_services.py |
pytest scripts/tests/test_<name>.py + pytest cloud/tests -q |
ind/<slug>-api |
web/app/api/<name>/route.ts, web/lib/<name>.ts, web/lib/use<Name>.ts, web/lib/serviceHealthWindows.ts entry + its pin test update, web/lib/refreshSchedule.ts <NAME>_REFRESH constant + its web/tests/refresh-schedule.test.ts case (mirrors the timer the ingestion worktree writes; both copy the OnCalendar from the spec) |
bunx vitest run --config vitest.config.ts web/tests/<name>-api.test.ts web/tests/service-health-windows.test.ts web/tests/refresh-schedule.test.ts |
ind/<slug>-ui |
web/components/<Name>Panel.tsx (with the <FreshnessRail> mount), RegimePanel.tsx registration (all four places), web/app/regime/<slug>/page.tsx, web/tests/regime-tab-routes.test.tsx update, web/e2e/<name>-tab.spec.ts |
bunx vitest run --config vitest.config.ts web/tests/<name>-panel.test.tsx web/tests/regime-tab-routes.test.tsx |
Mechanics:
git worktree add .claude/worktrees/<slug>-ingestion -b ind/<slug>-ingestion HEAD
# same for -api and -ui
Copy the spec + that worktree's failing tests into each worktree, then launch three
implementer subagents in parallel, one per worktree. Each prompt must include: the
worktree path (work ONLY there), the spec file, the reference implementation to copy
(fetch_margin_debt.py / margin-debt/route.ts / MarginDebtPanel.tsx), its
ownership list (touch nothing else), the exact test command, and the loop contract:
run tests → fix → rerun until its suite passes, then commit on its branch (scoped
git add of named files only). The UI implementer mocks the hook in unit tests, so it
does not need the API worktree's files to go green; TypeScript integration is checked
after merge.
Step 4 — Merge + full suite (integration green)
git worktree add .claude/worktrees/<slug>-merge -b ind/<slug> HEAD
cd .claude/worktrees/<slug>-merge
git merge --no-ff ind/<slug>-ingestion ind/<slug>-api ind/<slug>-ui # or sequentially
Resolve any conflicts (should be none if ownership held), add the spec file, then run
the FULL gates exactly as CI does, from the merge worktree root:
python -m pytest scripts/tests scripts/api/tests scripts/trade_blotter -q
python -m pytest cloud/tests -q
bun install --frozen-lockfile && (cd web && bun install --frozen-lockfile)
bunx vitest run --config vitest.config.ts
(cd web && npm run typecheck)
Fix integration failures here yourself (type mismatches across worktree seams, missed
lockstep pins, coverage ratchet). Do not loosen a pin test to get green.
Step 5 — Live browser verification (honest freshness)
- Run the real ingestion once against Turso (laptop writes direct-to-cloud) so the
tab has production data:
python scripts/fetch_<name>.py --json | head. Verify the
rows in Turso (not the JSON file).
- Start the dev server from the merge worktree (
cd web && npx next dev -p 3100 or
let Playwright's webServer do it with PLAYWRIGHT_PORT). Never kill 3000/8321/8765.
- Run the Playwright spec, plus a screenshot pass:
page.goto("/regime/<slug>") against the live API (no route mocks) →
page.screenshot({ path: "docs/indicators/<slug>-tab.png", fullPage: false }).
- Assert on the live page: the chart
<svg> has stroked paths (real data, not the
empty state); the header clock/SOURCE UPDATED cell shows the actual ingest/source
timestamps; the freshness rail ([data-testid="<name>-freshness-rail"]) shows
As of <data_date> and a ticking Next sample countdown whose target equals the
next OnCalendar slot of cloud/services/radon-<name>.timer (compute it from the
unit file, do not eyeball it); and no copy claims a cadence the backend does not
meet — grep the new UI strings for Refresh|Updated|hourly|daily|5m and check
each against the real timer OnCalendar. Screenshot both themes if the change
touches theme tokens.
Step 6 — Ship and verify production
- Bring the verified branch onto main: from the main worktree,
git merge ind/<slug>
(or cherry-pick the squashed commits). Stage nothing extra — operator WIP stays
untouched; git status before committing. Commit style:
feat(regime): <NAME> indicator - ingestion, API, chart tab (+ the Claude Code
trailer). Do not commit while a deploy is in flight.
- Push once.
gh run watch (or poll gh run list --workflow=ci.yml --limit 1) to
green — the deploy job runs in the same workflow.
- Verify production: migration applied (
schema_migrations has the new version),
Turso has the snapshot + history rows, prod API returns the payload (expect the
auth perimeter as anon), and load https://app.radon.run/regime/<slug> in a real
browser session for a final screenshot.
- Confirm the deploy installed and enabled the timer (
systemctl list-timers radon-<name>.timer;
the deploy log prints install-units: installed=2 when the manifest carried both
hashes), then trigger one run (systemctl start radon-<name>.service as root) and
check the service_health row.
- Clean up:
git worktree remove the four worktrees, delete the ind/* branches,
update tasks/todo.md review section.
Deliverables to show the user: red test output (Step 2), per-worktree green (Step 3),
full-suite green (Step 4), the tab screenshot with real data (Step 5), CI run URL +
production evidence (Step 6).
1---2name: indicator3description: Parallel TDD swarm that ships a new market indicator end to end. Usage - /indicator <data-source> <indicator-name>. Researches the source, writes a spec plus failing tests, fans out three worktree implementer subagents (ingestion, API, chart tab), merges, runs the full suite, screenshots the live tab with Playwright, then commits, pushes, waits for CI green, and verifies production.4---56# /indicator — parallel TDD swarm for a new market indicator78Arguments: `<data-source>` (URL or short description) and `<indicator-name>`.9Prerequisite: read `.claude/skills/new-indicator/SKILL.md` first — it defines the10target pattern, the lockstep pins, and the CI gates. Every step below produces11verifiable evidence before the next step starts.1213Derive up front and state them: `slug` (route segment, short), `service` (kebab-case,14used for `scan_snapshots.service`, `service_health`, and systemd unit names), `Name`15(PascalCase for components), tab label (UPPERCASE, no em dashes), and the next free16migration number (`ls scripts/db/migrations | sort | tail -1`).1718## Step 1 — Research subagent (blocking)1920Spawn one research subagent (general-purpose; give it WebFetch/curl) to confirm, with21evidence pasted back:2223- **Source**: exact URL(s), transport (XML/JSON/CSV/XLSX), auth (must be none or an24 existing repo credential), and a captured sample saved to25 `scripts/tests/fixtures/<name>_sample.<ext>` (this becomes the pytest fixture).26- **Schema**: field names, units, date keying, history depth, how far back a backfill27 can go, and any splice/seam issues between historical regimes.28- **Update cadence**: when the source actually publishes (day of week/month, time,29 timezone) — this dictates the timer OnCalendar, `MAX_AGE_MS`, staleness windows,30 and what freshness copy is honest.31- **Licensing**: US-government/public-domain data is fine; otherwise confirm terms32 permit storage + display. Record verdict in the spec. Stop and ask the user if33 licensing is unclear.34- **Data-source priority**: note why IB/UW do not already serve this series (repo rule:35 IB → UW → Yahoo → scrape).3637Reject the indicator here if the source needs browser impersonation, scraping behind38auth, or has hostile terms.3940## Step 2 — Spec + failing tests (red)4142Write `docs/indicators/<slug>.md`: signal definition and thresholds, source facts from43Step 1, payload shape (`scan_time`, `source_last_modified`, `current`, `series[]`),44migration DDL, API contract (missing object, cache headers, MAX_AGE reasoning), UI spec45(strip cells, chart series/axes, presets, copy strings including tooltip), the46**freshness rail** (the `<NAME>_REFRESH` constant that mirrors the timer's47`OnCalendar`, the `<FreshnessRail>` mount under the strip with its two testIds, and48which payload field feeds `asOf`), timer cadence, and the exact file checklist per49the pattern skill. The countdown is not optional: an indicator with a timer always50shows when its source is next sampled, and the spec must name the constant.5152Then write the failing tests against the spec (implementation files do not exist yet):5354- `scripts/tests/test_<name>.py` — parse the captured fixture, transforms, payload55 contract, migration schema pin, conditional-GET/heartbeat behavior, writer arity.56- `web/tests/<name>-api.test.ts` — dbFirstRead behavior + missing contract.57- `web/tests/<name>-panel.test.tsx` — loader/empty/strip/chart/chips + NaN guard.5859Run all three suites and **record the red output**. Failing on import/missing-module is60the expected red for greenfield files.6162## Step 3 — Three worktrees, parallel implementers6364Partition by ownership so merges are near-disjoint:6566| Worktree branch | Owns | Its tests |67|---|---|---|68| `ind/<slug>-ingestion` | `scripts/fetch_<name>.py`, client, migration, `scripts/db/writer.py` additions, `scripts/watchdog/services.py`, `cloud/services/radon-<name>.{service,timer}`, `setup-vps.sh` array, `cloud/tests/test_systemd_services.py` | `pytest scripts/tests/test_<name>.py` + `pytest cloud/tests -q` |69| `ind/<slug>-api` | `web/app/api/<name>/route.ts`, `web/lib/<name>.ts`, `web/lib/use<Name>.ts`, `web/lib/serviceHealthWindows.ts` entry + its pin test update, `web/lib/refreshSchedule.ts` `<NAME>_REFRESH` constant + its `web/tests/refresh-schedule.test.ts` case (mirrors the timer the ingestion worktree writes; both copy the `OnCalendar` from the spec) | `bunx vitest run --config vitest.config.ts web/tests/<name>-api.test.ts web/tests/service-health-windows.test.ts web/tests/refresh-schedule.test.ts` |70| `ind/<slug>-ui` | `web/components/<Name>Panel.tsx` (with the `<FreshnessRail>` mount), `RegimePanel.tsx` registration (all four places), `web/app/regime/<slug>/page.tsx`, `web/tests/regime-tab-routes.test.tsx` update, `web/e2e/<name>-tab.spec.ts` | `bunx vitest run --config vitest.config.ts web/tests/<name>-panel.test.tsx web/tests/regime-tab-routes.test.tsx` |7172Mechanics:7374```bash75git worktree add .claude/worktrees/<slug>-ingestion -b ind/<slug>-ingestion HEAD76# same for -api and -ui77```7879Copy the spec + that worktree's failing tests into each worktree, then launch three80implementer subagents **in parallel**, one per worktree. Each prompt must include: the81worktree path (work ONLY there), the spec file, the reference implementation to copy82(`fetch_margin_debt.py` / `margin-debt/route.ts` / `MarginDebtPanel.tsx`), its83ownership list (touch nothing else), the exact test command, and the loop contract:84run tests → fix → rerun **until its suite passes**, then commit on its branch (scoped85`git add` of named files only). The UI implementer mocks the hook in unit tests, so it86does not need the API worktree's files to go green; TypeScript integration is checked87after merge.8889## Step 4 — Merge + full suite (integration green)9091```bash92git worktree add .claude/worktrees/<slug>-merge -b ind/<slug> HEAD93cd .claude/worktrees/<slug>-merge94git merge --no-ff ind/<slug>-ingestion ind/<slug>-api ind/<slug>-ui # or sequentially95```9697Resolve any conflicts (should be none if ownership held), add the spec file, then run98the FULL gates exactly as CI does, from the merge worktree root:99100```bash101python -m pytest scripts/tests scripts/api/tests scripts/trade_blotter -q102python -m pytest cloud/tests -q103bun install --frozen-lockfile && (cd web && bun install --frozen-lockfile)104bunx vitest run --config vitest.config.ts105(cd web && npm run typecheck)106```107108Fix integration failures here yourself (type mismatches across worktree seams, missed109lockstep pins, coverage ratchet). Do not loosen a pin test to get green.110111## Step 5 — Live browser verification (honest freshness)1121131. Run the real ingestion once against Turso (laptop writes direct-to-cloud) so the114 tab has production data: `python scripts/fetch_<name>.py --json | head`. Verify the115 rows in Turso (not the JSON file).1162. Start the dev server from the merge worktree (`cd web && npx next dev -p 3100` or117 let Playwright's webServer do it with `PLAYWRIGHT_PORT`). Never kill 3000/8321/8765.1183. Run the Playwright spec, plus a screenshot pass:119 `page.goto("/regime/<slug>")` against the live API (no route mocks) →120 `page.screenshot({ path: "docs/indicators/<slug>-tab.png", fullPage: false })`.1214. Assert on the live page: the chart `<svg>` has stroked paths (real data, not the122 empty state); the header clock/`SOURCE UPDATED` cell shows the actual ingest/source123 timestamps; the freshness rail (`[data-testid="<name>-freshness-rail"]`) shows124 `As of <data_date>` and a ticking `Next sample` countdown whose target equals the125 next `OnCalendar` slot of `cloud/services/radon-<name>.timer` (compute it from the126 unit file, do not eyeball it); and **no copy claims a cadence the backend does not127 meet** — grep the new UI strings for `Refresh|Updated|hourly|daily|5m` and check128 each against the real timer OnCalendar. Screenshot both themes if the change129 touches theme tokens.130131## Step 6 — Ship and verify production1321331. Bring the verified branch onto main: from the main worktree, `git merge ind/<slug>`134 (or cherry-pick the squashed commits). Stage nothing extra — operator WIP stays135 untouched; `git status` before committing. Commit style:136 `feat(regime): <NAME> indicator - ingestion, API, chart tab` (+ the Claude Code137 trailer). Do not commit while a deploy is in flight.1382. Push once. `gh run watch` (or poll `gh run list --workflow=ci.yml --limit 1`) to139 green — the deploy job runs in the same workflow.1403. Verify production: migration applied (`schema_migrations` has the new version),141 Turso has the snapshot + history rows, prod API returns the payload (expect the142 auth perimeter as anon), and load `https://app.radon.run/regime/<slug>` in a real143 browser session for a final screenshot.1444. Confirm the deploy installed and enabled the timer (`systemctl list-timers radon-<name>.timer`;145 the deploy log prints `install-units: installed=2` when the manifest carried both146 hashes), then trigger one run (`systemctl start radon-<name>.service` as root) and147 check the `service_health` row.1485. Clean up: `git worktree remove` the four worktrees, delete the `ind/*` branches,149 update `tasks/todo.md` review section.150151Deliverables to show the user: red test output (Step 2), per-worktree green (Step 3),152full-suite green (Step 4), the tab screenshot with real data (Step 5), CI run URL +153production evidence (Step 6).