# Tech Stack — ForgeAI Studio
> Owner: Anthony Kautz · Updated: 2026-08-04

---

## Overview

ForgeAI Studio is intentionally low-infrastructure. There is no build system, no package manager, and generation itself has no backend server. The entire generation toolchain runs on three layers: a small multi-page HTML/JS/CSS app (four hand-authored pages sharing one app shell, no bundler), a Markdown-based artifact system, and the Anthropic API.

One deliberate exception: `persona-library-server/` (see below) is a small local backend + database for *storing and retrieving* persona packs after they've been run through the pipeline — generation, red-team, and eval logic remain entirely client-side.

---

## App Shell — `index.html`, `pipeline.html`, `library.html`, `docs.html`

Four pages (Landing, Pipeline, Persona Library, Docs) sharing a persistent top bar + left nav sidebar. `app-shell.js` renders that nav into each page's `#shell-topbar`/`#shell-sidebar` mount points; `shell.css` holds the design tokens and all component styles used across the four pages. This pair is the one deliberate shared-file exception — every other structural/behavioral decision stays self-contained in that page's own `.html` file, per `context/coding-standards.md`.

On `pipeline.html`, the outer nav sidebar collapses to an icon-only rail (`.shell-sidebar.icon-collapsed`) so it doesn't compete with the pipeline's own internal 8-stage sidebar. "Load into Pipeline" from `library.html` hands a persona off to `pipeline.html` via a one-shot `localStorage` key (`orion_library_handoff`) rather than a shared in-page JS state, since the two are now separate page loads.

| Concern | Technology |
|---|---|
| Runtime | Browser — vanilla HTML5, CSS3, ES2022 |
| JavaScript | Vanilla JS (no framework, no bundler) |
| Styling | CSS custom properties (design tokens); no preprocessor |
| Fonts | JetBrains Mono (labels, code, output) · Inter (body copy) — loaded via Google Fonts CDN |
| Build step | None — plain `.html`/`.css`/`.js` files, open directly in any modern browser |
| State management | In-memory JS object (`pd`) per page — no persistence, except the one-shot library→pipeline handoff above |
| API calls | `fetch()` → Anthropic Messages API (`https://api.anthropic.com/v1/messages`) from `pipeline.html`; `fetch()` → `persona-library-server/` from `library.html` and `pipeline.html`'s Save-to-Library modal |
| Auth | Anthropic API key from `window.ANTHROPIC_API_KEY`, set either via `pipeline.html`'s own header API key control (pasted key, held in `sessionStorage` for that tab only) or a local `config.local.js` (gitignored, copied from `config.local.js.example`) — the header control takes precedence when both are present |
| Clipboard | `navigator.clipboard` API for copy-to-clipboard |
| File download | `Blob` + `URL.createObjectURL` for `.md` downloads |
| Design standard | Vercel-style dark UI — true black base, stepped surfaces, `#00ff9d` accent |

---

## LLM APIs

| Provider | Models used | Purpose |
|---|---|---|
| **Anthropic** | `claude-sonnet-5` | Persona pack generation, system prompt derivation, red-team, eval pack, judge prompt |
| **OpenAI** | `gpt-4o` | Not a provider this tool calls, by design — `gpt-4o` is a Stage 01 Target Model option describing the *delivered persona's* deployment target, distinct from what Orion itself runs on (see `context/features/archive/multi-model-provider-support-feature.md`) |

API calls are made directly from the browser (client-side). No backend proxy. The API key comes from `pipeline.html`'s own header API key control or a local, gitignored `config.local.js` (see App Shell table above) — never sent anywhere but directly to Anthropic.

**Default generation parameters:**
| Parameter | Default | Notes |
|---|---|---|
| Model | `claude-sonnet-5` | Hardcoded at every call site — not a UI setting. (Stage 01's Target Model dropdown describes the *persona being designed*'s intended deployment model, a separate concept from what Orion itself runs on.) |
| Temperature | Not set — no calls send a `temperature` parameter | `claude-sonnet-5` rejects it outright ("deprecated for this model"); there is no API knob for determinism on any call, including the Judge/Eval calls, which instead rely on prompt instruction plus a hard-gate pass/fail computed in code from the resulting scores |
| Max tokens | 1024 | Configurable per persona |

---

## Artifact System

All pipeline artifacts are plain Markdown files with a structured schema — no database, no CMS.

| Artifact type | Format | Versioning |
|---|---|---|
| Persona Pack | `.md` | `name@MAJOR.MINOR.PATCH.md` |
| Eval Pack | `.md` | `name-eval-pack@MAJOR.MINOR.PATCH.md` |
| Judge Prompt | `.md` | `judge-name@MAJOR.MINOR.PATCH.md` |
| Registry entries | `.md` | Date-stamped filenames |

**Schema standard:** ForgeAI PersonaForge draft-07 (custom schema; not JSON Schema — Markdown-native)

Every artifact carries a mandatory tag block:
```
`name@version` | eval-status | temp | max_tokens | red-team state | bias facet | eval-pack state
```

---

## Persona Library Backend — `persona-library-server/`

The one backend/database in the repo — added to let a persona pack (and later pipeline-stage output) survive past the browser session as a queryable store, separate from the git-committed Markdown artifacts above. See `context/features/persona-storage-feature.md` for the full spec.

| Concern | Technology |
|---|---|
| Runtime | Node.js ≥22.5 |
| HTTP framework | Express |
| Database | `node:sqlite` (Node's built-in `DatabaseSync`) — no native dependency, no `better-sqlite3` |
| Data file | `persona-library-server/data/personas.db` — local, gitignored |
| Start | `npm install && npm start` in `persona-library-server/` — runs on `http://localhost:4100` |
| CORS | Wide open (`Access-Control-Allow-Origin: *`) — the pipeline UI is a `file://` page with no fixed origin |

**API**: `POST/GET /personas`, `GET /personas/{name}`, `GET /personas/{name}/{version}`, `DELETE /personas/{name}/{version}`. Re-saving an existing `name@version` returns `409` — versions are immutable once stored, matching this repo's semver convention (see Artifact System above).

**Relationship to the Markdown artifact system**: saving to the library never reads, writes, or deletes anything under `personas/`, `eval-packs/`, `judge-prompts/`, or `registry/`. It's an additional working/staging index — including drafts that never get promoted — not a replacement for the git-reviewed artifacts. `pipeline.html`'s **💾 Save to Library** action (Stage 02 Persona Pack, System Prompt, and Judge Prompt panels) POSTs to it; `library.html` — a full page reachable from the top-level nav, not an in-pipeline modal — browses/filters/loads/downloads/deletes from it.

**Real Stage 06/07 run persistence**: `persona_versions` also carries `eval_results_json`/`eval_axis_scores_json`/`eval_ran_at`/`judge_verdict_json`/`judge_ran_at`, populated whenever a Save to Library happens after a real "Run Eval"/"Run Judge" pass. Loading that version back into the pipeline (from `library.html` or the cross-page handoff) restores those results into the running session, so a completed run can be reviewed again without re-paying for it. See `context/features/archive/eval-run-resilience-feature.md`.

---

## Hosting

Two independent deployments, kept deliberately separate rather than combined onto one platform:

| Piece | Platform | Cost |
|---|---|---|
| Static frontend (`index.html`, `pipeline.html`, `library.html`, `docs.html`, `shell.css`, `app-shell.js`, and the Markdown files `docs.html` links to) | Cloudflare Pages | $0 — free tier, works with a private GitHub repo |
| `persona-library-server/` (Express + `node:sqlite`) | Fly.io | ~$2–4/mo (smallest machine, scales to zero when idle) — pay-as-you-go, no flat free tier |

**Why two platforms instead of one**: Railway was the original plan (one flat $5/mo bill for both pieces combined), but was dropped after Railway's October 21, 2026 security incident, in which an attacker reportedly accessed decrypted customer environment variables. Cloudflare Pages needs no backend secrets at all (pure static hosting), and Fly.io has no equivalent recent incident — splitting the two avoids putting this project's only server-side credentials on a platform with an open, unresolved breach.

**Frontend (Cloudflare Pages)**: deploys from `_site/`, built by `npm run build:pages` (`scripts/build-pages-site.js`), which copies only the files the four HTML pages actually reference. This is deliberate, not incidental — a static host publishes whatever lands in its output directory, so pointing Cloudflare at the repo root as-is would make `context/`, `.claude/`, `tests/`, and `persona-library-server/`'s source all publicly fetchable by URL despite this being a private repo. Cloudflare's own GitHub integration rebuilds and redeploys automatically on every push — no GitHub Actions workflow needed for this half.

**Backend (Fly.io)**: `persona-library-server/Dockerfile` + `fly.toml` (`node:22-slim`, smallest `shared-cpu-1x`/256MB machine, `min_machines_running = 0` so it scales to zero between requests rather than billing 24/7). `personas.db` lives on a mounted Fly volume (`DATA_DIR` env var, see `persona-library-server/db.js`) so it survives redeploys — without that mount it would live on the container's ephemeral disk and be wiped on every deploy. `.github/workflows/fly-deploy.yml` runs `flyctl deploy` on every push to `main` that touches `persona-library-server/`, gated on a `FLY_API_TOKEN` repository secret (set once, manually, after `fly launch`).

**Wiring the two together**: `app-shell.js`'s `PERSONA_LIBRARY_API` resolves to `http://localhost:4100` when the page is opened via `file://` or served from `localhost`/`127.0.0.1` (matching local dev and the `tests/` suite's `static-server.js` + `persona-library-server` split), and to the deployed Fly.io URL otherwise. That URL is a placeholder (`orion-persona-library.fly.dev`) until the real one `fly launch` assigns is substituted in.

---

## Test Suite — `tests/`

The second deliberate Node exception in the repo (after `persona-library-server/`) — a committed, re-runnable Playwright E2E suite, added because every prior feature's "verified end-to-end" claim was a throwaway script, written once and never committed, leaving nothing to catch a later feature silently regressing an earlier one. See `context/features/archive/e2e-testing-feature.md`.

| Concern | Technology |
|---|---|
| Runtime | Node.js, Playwright (`@playwright/test`) |
| Scope | E2E only, against a real rendered browser page — no unit tests. This repo's functions live inside one `<script>` block per HTML file, not as importable modules |
| API calls | Mocked by default via `page.route()` (`tests/helpers/mockAnthropic.js`) — fast, free, deterministic; no live Anthropic key or spend required to run the suite |
| `persona-library-server` | Run for real, not mocked — the point of `persona-library.spec.js` is proving the real HTTP/DB contract, the one deliberate backend in the project |
| Start | `cd tests && npm install && npx playwright install chromium && npm test` |
| CI | Runs automatically on every push to `main` via `.github/workflows/e2e-tests.yml` — no live Anthropic key needed (mocked by default), informational check only (does not block merging). See `context/features/archive/ci-workflow-feature.md`. |

**Coverage**: Stage 01–08 pipeline flow including the real hard-gate enforcement (`pipeline-eval-judge.spec.js`, `pipeline-redteam-patch.spec.js` — the gate-failure test's mock deliberately self-reports `gate_pass: true` to prove the app overrides it in code rather than trusting the model), the persona-library-server save → browse → load → download → delete round trip (`persona-library.spec.js`), the 4-page app-shell navigation (`app-shell-nav.spec.js`), Stage 01's Import Profile modal (`import-profile.spec.js` — Mode A field-mapping import onto the 17 known Input Template fields with unrecognized fields preserved as Custom Fields, a regression case for `profile-import-feature`'s own review-caught `pd._importedFields` merge bug, and Mode B full-pack paste auto-detection into `PACK IMPORTED`), Stage 01's AI-suggested field options (`field-suggestions.spec.js` — the five "Suggest options" controls disabled until Domain/Audience/Mission are filled, real per-field Orion calls generating field-tailored candidates, the 7-selection cap, append-only merging, a real error with no offline fallback on a failed call, and a regression case for appending onto text typed but not yet synced into `pd`), the header API key control (`api-key-control.spec.js` — session-key precedence over `config.local.js`, persistence across a reload, and the show/hide-preserves-unsaved-value regression), the Stage 08 Next Steps generators (`pipeline-next-steps.spec.js` — NS-01 through NS-04 via mocked live calls, and NS-05, which makes no API call at all and is asserted directly against real app state), and `withRetry()`/`isRetryableError()` under a simulated API failure (`pipeline-eval-resilience.spec.js`, via the new `installFlakyAnthropicMock()` helper — retry-then-succeed, fail-fast on a non-retryable 4xx, one hard-gate axis failing closed while the rest score correctly). No remaining known gaps in this suite's own stated scope.

---

## Version Control

| Tool | Usage |
|---|---|
| Git | All artifacts versioned; filename contains semantic version |
| GitHub | Remote: `https://github.com/TonyKz/Orion` |
| Branch strategy | Feature branches per `/feature start`, merged into `main` and deleted per `/feature complete` — not a single-branch workflow |
| App/tool version | Root `VERSION` file (`major.minor.patch`), bumped automatically by `/feature complete` — patch for a spec from `context/fixes/`, minor for one from `context/features/`, major only on an explicit `Breaking: yes` in the spec. Shown as a pill in the shared app shell's top bar (`app-shell.js`). Distinct from persona `name@version` tagging (e.g. `orion@2.2.0`) — this versions the tool itself, not the personas it produces. See `context/features/archive/app-version-tracking-feature.md`. |

---

## Evaluation Infrastructure (current)

Design-time evaluation is manual: Orion applies the scoring rubric against the persona spec and records results in Markdown registry files.

| Component | Current state |
|---|---|
| Eval cases | Markdown (`.md`) in `eval-packs/` |
| Scoring | Manual — Orion applies rubric, records in `registry/` |
| Judge Prompt | `judge-prompts/sage-judge@1.0.0.md` — real, built via a real Stage 07 run against `sage@1.0.0`'s real Stage 06 eval results (both hard gates cleared). First real content in this directory. |
| Hard gate enforcement | Manual check against eval run results |

`pipeline.html`'s own Stages 04–08 are a separate, real path from this manual process: "Run Red-Team"/"Run Patch"/"Run Eval"/"Run Judge" all live-generate their own inputs (probes, cases — not drawn from any persona's committed eval-pack Markdown) and compute genuine findings, per-axis scores, and a hard-gate verdict, independent of the manual registry-based process described here. See `context/fixes/archive/judge-prompt-not-executed-fix.md`, `context/features/archive/real-eval-judge-execution-feature.md`, and `context/features/archive/real-red-team-execution-feature.md`. `eval-pack@1.0.0.md`/`judge-morgan@0.0.0-scaffold.md` — the placeholder scaffolds for a `morgan` persona that was never actually developed — were removed entirely rather than kept as superseded reference; see `context/features/archive/eval-pack-artifact-fate-feature.md` and the "Remove Morgan Persona" entry in `context/current-feature.md`'s History.

---

## Evaluation Infrastructure (target)

| Component | Target tooling |
|---|---|
| Eval harness | PromptFoo · LangSmith |
| Schema validation | Ajv (JSON Schema) · Zod (TypeScript) |
| Prompt optimization | DSPy · PromptLayer · Latitude |
| Judge verdict format | Structured JSON (schema in `judge-prompts/`) |

**CI/CD gate is not a target — it's already real**, just scoped differently than this table originally implied: Stage 08's "Next Steps" panel live-generates an eval-pass-rate-gated CI/CD config (`runNs01()`, `pipeline.html:2355-2368`) for the *persona's own downstream deployment repo*, not for gating merges to this repo (which has no fixed golden-set persona to check against). See `context/features/archive/cicd-eval-gate-feature.md`.

---

## Developer Environment

| Concern | Detail |
|---|---|
| OS | macOS |
| Editor | VS Code |
| AI assistant | GitHub Copilot (Claude Sonnet 4.6) |
| Dependencies | The UI itself (`pipeline.html`/`index.html`/`library.html`/`docs.html`) is plain HTML/CSS/JS — no `npm install`, `pip install`, or build step. `persona-library-server/` and `tests/` are each a separate, deliberate Node exception and do need `npm install`. |

---

## What Is Deliberately Absent

- **No backend / server for generation** — all generation is client-side API calls; no data leaves the browser except to Anthropic. `persona-library-server/` is the one exception, and it only stores/retrieves already-generated artifacts — it never calls Anthropic or performs generation itself.
- **No database for the git-tracked artifact system** — `personas/`, `eval-packs/`, `judge-prompts/`, `registry/` remain plain Markdown files in the repo. The library backend's SQLite database is a separate, additive index (see "Persona Library Backend" above), not a replacement.
- **No auth system** — single-operator tool; API key is runtime-supplied
- **No linter** — no enforced code-style tooling on the HTML/CSS/JS. A real E2E test suite does exist (`tests/`, Playwright — see "Test Suite" below); pipeline *behavioral* quality (persona voice/safety/bias) still runs through the eval harness, not these tests, which cover the tool's own code paths instead
- **No mobile support** — UI is desktop-only in the current version
